package com.study.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.study.cacheManager.LinkCacheManager;
import com.study.config.GotoDomainWhiteListConfiguration;
import com.study.constant.RedisKeyConstant;
import com.study.constant.RedissonLockConstant;
import com.study.convention.errorCode.BaseErrorCode;
import com.study.convention.exception.ClientException;
import com.study.convention.exception.ServiceException;
import com.study.dao.entity.TLink;
import com.study.dao.entity.TLinkGoto;
import com.study.dao.mapper.TLinkGotoMapper;
import com.study.dao.mapper.TLinkMapper;
import com.study.dao.mapper.TLinkStatsTodayMapper;
import com.study.dto.biz.ShortLinkStatsRecordDTO;
import com.study.dto.req.ShortLinkCreateReqDTO;
import com.study.dto.req.ShortLinkPageReqDTO;
import com.study.dto.resp.ShortLinkCreateRespDTO;
import com.study.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.study.dto.resp.ShortLinkPageRespDTO;
import com.study.service.ShortLinkService;
import com.study.service.ShortLinkStatsService;
import com.study.utils.HashUtil;
import com.study.utils.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author canace
 * @version 1.0
 * @description 短链接服务实现类
 * @date 2024/1/30 20:58
 */

@Service
@RequiredArgsConstructor
@Slf4j
public class ShortLinkServiceImpl extends ServiceImpl<TLinkMapper, TLink> implements ShortLinkService {

    private final TLinkGotoMapper linkGotoMapper;

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;

    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    private final RedissonClient redissonClient;

    private final StringRedisTemplate stringRedisTemplate;

    private final LinkCacheManager linkCacheManager;

    private final ShortLinkStatsService shortLinkStatsService;

    private final TLinkStatsTodayMapper linkStatsTodayMapper;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;


    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {
        // 原始链接是否在白名单中
        verificationWhitelist(requestParam.getOriginUrl());
        // 生成短链接后缀
        String shortLink = generateShortLink(requestParam.getOriginUrl());
        // 拼接短链接
        String fullShortLink = StrBuilder.create(createShortLinkDefaultDomain).append("/").append(shortLink).toString();
        TLink tLink = TLink.builder().domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(LocalDateTime.ofInstant(requestParam.getValidDate().toInstant(), ZoneId.systemDefault()))
                .description(requestParam.getDescribe())
                .shortUri(shortLink)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .fullShortUrl(fullShortLink)
//                .favicon(getFavicon(requestParam.getOriginUrl())) // 省略
                .build();
        TLinkGoto tLinkGoto = TLinkGoto.builder().fullShortUrl(fullShortLink)
                .gid(requestParam.getGid())
                .build();

        // 分布式锁，防止重复入库，保证操作的原子性
        RLock lock = redissonClient.getLock(RedissonLockConstant.LOCK_SHORT_LINK_CREATE_KEY + requestParam.getGid());
        try {
            if (lock.tryLock()) {
                try {
                    int insertLink = baseMapper.insert(tLink);
                    int insertLinkGoTo = linkGotoMapper.insert(tLinkGoto);
                    if (insertLink < 1 || insertLinkGoTo < 1) {
                        throw new ServiceException(BaseErrorCode.SHORT_LINK_ADD_ERROR);
                    }
                } catch (DuplicateKeyException ex) {
                    log.warn("短链接：{} 重复入库", fullShortLink);
                    throw new ServiceException(BaseErrorCode.SHORT_LINK_IS_EXIST_ERROR);
                }
            }
        } finally {
            lock.unlock();
        }
        // 将短链接存入redis缓存中
        stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortLink), requestParam.getOriginUrl(), LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS);

        // 将短链接存入布隆过滤器中
        shortUriCreateCachePenetrationBloomFilter.add(fullShortLink);

        // 返回封装结果
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + tLink.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 1.拼接短链接，获取完整短链接
        String fullShortLink = StrBuilder.create(createShortLinkDefaultDomain).append("/").append(shortUri).toString();

        // 2.从缓存中获取原始链接信息
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortLink));
        // 如果缓存中存在，则直接跳转
        if (StrUtil.isNotBlank(originalLink)) {
            // 从用户的请求中中获取用户统计信息
            ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortLink, request, response);
            // 将统计信息插入数据库
            shortLinkStatsService.insertShortLinkStats(fullShortLink, null, shortLinkStatsRecordDTO);
            // 直接跳转返回原始链接
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }

        // 如果缓存中不存在，则从数据库中获取
        // 先从布隆过滤器查询是否存在该短链接，不存在则表示短链接不存在
        if (!shortUriCreateCachePenetrationBloomFilter.contains(fullShortLink)) {
            // 不存在则跳转至页面不存在界面
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 查询redis中短链接空值是否存在，存在则表示短链接不存在，直接返回
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortLink));
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 从数据库中查询短链接
        LambdaQueryWrapper<TLinkGoto> linkGotoQueryWrapper = Wrappers.lambdaQuery(TLinkGoto.class)
                .eq(TLinkGoto::getFullShortUrl, fullShortLink);
        TLinkGoto tLinkGoto = linkGotoMapper.selectOne(linkGotoQueryWrapper);
        if (Objects.isNull(tLinkGoto)) {
            // 如果数据库中不存在，则将短链接空值存入redis中
            stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortLink), "-", 30, TimeUnit.MINUTES);
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 从数据库查询短链接有效期
        LambdaQueryWrapper<TLink> linkLambdaQueryWrapper = Wrappers.lambdaQuery(TLink.class)
                .eq(TLink::getGid, tLinkGoto.getGid())
                .eq(TLink::getFullShortUrl, fullShortLink)
                .eq(TLink::getDelFlag, 0)
                .eq(TLink::getEnableStatus, 0);
        TLink tLinkOne = baseMapper.selectOne(linkLambdaQueryWrapper);
        if (Objects.isNull(tLinkOne) || (!Objects.isNull(tLinkOne.getValidDate()) && tLinkOne.getValidDate().isBefore(LocalDateTime.now()))) {
            // 如果数据库中不存在或者以过期，则将短链接空值存入redis中
            stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortLink), "-", 30, TimeUnit.MINUTES);
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        // 将结果存入redis中
        stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortLink), tLinkOne.getOriginUrl(), LinkUtil.getLinkCacheValidTime(Date.from(tLinkOne.getValidDate().atZone(ZoneId.systemDefault()).toInstant())), TimeUnit.MILLISECONDS);
        // 从用户的请求中中获取用户统计信息
        ShortLinkStatsRecordDTO shortLinkStatsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortLink, request, response);
        // 将统计信息插入数据库
        shortLinkStatsService.insertShortLinkStats(fullShortLink, tLinkOne.getGid(), shortLinkStatsRecordDTO);
        // 直接跳转返回原始链接
        ((HttpServletResponse) response).sendRedirect(tLinkOne.getOriginUrl());
    }

    /**
     * 获取用户的统计信息
     *
     * @param fullShortLink 完整的短链接
     * @param request       用户的请求
     * @param response      用户的响应
     * @return 用户的统计信息
     */
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortLink, ServletRequest request, ServletResponse response) {
        // 创建一个原子布尔值，用于判断当前用户是否第一次访问（线程安全）
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 获取用户的cookie
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        // 创建一个线程安全的引用类型，用于存储用户的uv
        AtomicReference<String> uv = new AtomicReference<>();
        // 创建一个并行的任务，用于给用户添加一个uv的cookie
        Runnable addResponseCookieTask = () -> {
            // 给用户定义一个唯一的uuid
            uv.set(UUID.fastUUID().toString());
            // 创建一个uv的cookie
            Cookie uvCookie = new Cookie("uv", uv.get());
            // 设置cookie的有效期为30天
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            // 设置cookie的路径
            uvCookie.setPath(StrUtil.sub(fullShortLink, fullShortLink.indexOf("/"), fullShortLink.length()));
            // 将cookie添加到用户的响应中
            ((HttpServletResponse) response).addCookie(uvCookie);
            // 设置用户的uv为第一次访问
            uvFirstFlag.set(Boolean.TRUE);
            // 将用户的uv存入redis中
            stringRedisTemplate.opsForSet().add(String.format(RedisKeyConstant.SHORT_LINK_UV_STATS_KEY, fullShortLink), uv.get());
        };
        // 检查用户是否有cookie，有则从cookie中获取uv，没有则添加一个uv的cookie
        if (ObjectUtil.isNotEmpty(cookies)) {
            // 从cookie中获取uv
            Arrays.stream(cookies).filter(each -> "uv".equals(each.getName())).findFirst().map(Cookie::getValue).ifPresentOrElse(ea -> {
                // 如果cookie中存在uv，则将uv存入引用类型中
                uv.set(ea);
                // 将uv存入redis中
                Long add = stringRedisTemplate.opsForSet().add(String.format(RedisKeyConstant.SHORT_LINK_UV_STATS_KEY, fullShortLink), ea);
                // 如果存入成功，则表示用户是第一次访问
                uvFirstFlag.set(add != null && add > 0L);
            }, addResponseCookieTask);
        } else {
            // 如果用户的cookie为空，则直接添加一个uv的cookie
            addResponseCookieTask.run();
        }
        // 从用户的请求中中获取相关设备信息
        String actualIp = LinkUtil.getActualIp((HttpServletRequest) request);
        String browser = LinkUtil.getBrowser((HttpServletRequest) request);
        String os = LinkUtil.getOs((HttpServletRequest) request);
        String device = LinkUtil.getDevice((HttpServletRequest) request);
        String network = LinkUtil.getNetwork((HttpServletRequest) request);
        // 将用户真实ip存入到redis中
        Long uipAdded = stringRedisTemplate.opsForSet().add(String.format(RedisKeyConstant.SHORT_LINK_UIP_STATS_KEY, fullShortLink), actualIp);
        // 如果存入成功，则表示用户是第一次访问
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;

        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortLink)
                .uvFirstFlag(uvFirstFlag.get())
                .uv(uv.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(actualIp)
                .browser(browser)
                .os(os)
                .device(device)
                .network(network)
                .build();
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        // 改用从缓存中获取
        List<Map<String, Object>> shortLinkCountByGidS = linkCacheManager.getShortLinkCountByGidS(requestParam);
        if (ObjectUtil.isEmpty(shortLinkCountByGidS)) {
            throw new ServiceException(BaseErrorCode.SHORT_LINK_GROUP_COUNT_QUERY_ERROR);
        }
        return BeanUtil.copyToList(shortLinkCountByGidS, ShortLinkGroupCountQueryRespDTO.class);
    }

    @Override
    public IPage<ShortLinkPageRespDTO> getPageShortLink(ShortLinkPageReqDTO requestParam) {
//        // 分页查询基础的数据
//        LambdaQueryWrapper<TLink> queryWrapper = Wrappers.lambdaQuery(TLink.class)
//                .eq(TLink::getGid, requestParam.getGid())
//                .eq(TLink::getDelFlag, 0)
//                .orderByDesc(TLink::getCreateTime);
//        IPage<TLink> page = baseMapper.selectPage(requestParam, queryWrapper);
//
//        // 查询当天的pv,uip,uv
//        LambdaQueryWrapper<TLinkStatsToday> tLinkStatsTodayLambdaQueryWrapper = Wrappers.lambdaQuery(TLinkStatsToday.class)
//                .eq(TLinkStatsToday::getGid, requestParam.getGid())
//                .eq(TLinkStatsToday::getDate, DateUtil.today())
//                .eq(TLinkStatsToday::getDelFlag, 0)
//                .orderByDesc(TLinkStatsToday::getCreateTime);
//        List<TLinkStatsToday> tLinkStatsToday = linkStatsTodayMapper.selectList(tLinkStatsTodayLambdaQueryWrapper);
//
//        // convert 把分页记录进行list处理，然后进行转换
//        IPage<ShortLinkPageRespDTO> convert = page.convert(tLink -> {
//            ShortLinkPageRespDTO shortLinkPageRespDTO = BeanUtil.copyProperties(tLink, ShortLinkPageRespDTO.class);
//            TLinkStatsToday tLinkStatsTodayNew = tLinkStatsToday.stream().filter(item -> item.getFullShortUrl().equals(tLink.getFullShortUrl()))
//                    .findFirst()
//                    .orElse(new ShortLinkPageRespDTO());
//            shortLinkPageRespDTO.setTodayPv(tLinkStatsTodayNew.getTodayPv());
//            shortLinkPageRespDTO.setTodayUip(tLinkStatsTodayNew.getTodayUip());
//            shortLinkPageRespDTO.setTotalUv(tLinkStatsTodayNew.getTodayUv());
//            return shortLinkPageRespDTO;
//        });

        // 上述方法无法实现排序，这里采用自己写sql完成
        IPage<TLink> page = baseMapper.pageLink(requestParam);
        return page.convert(tLink -> BeanUtil.copyProperties(tLink, ShortLinkPageRespDTO.class));
    }

    /**
     * 根据原始链接生成短链接后缀
     *
     * @param originUrl 原始链接
     * @return 短链接后缀
     */
    private String generateShortLink(String originUrl) {
        int customGenerateCount = 0;
        String shorUri;
        // 最多生成10次，去生成一个不在布隆过滤器中的短链接
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            // 生成短链接
            originUrl += System.currentTimeMillis();
            shorUri = HashUtil.hashToBase62(originUrl);
            // 如果生成的短链接在布隆过滤器中，则重新生成
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

    /**
     * 校验跳转链接是否在白名单中
     *
     * @param originUrl 原始链接
     */
    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        // 如果白名单功能未开启，则直接返回
        if (ObjectUtil.isNull(enable) || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }
}
